package com.overto.gwt.hermes.rebind;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.overto.gwt.hermes.client.Cancellable;
import com.overto.gwt.hermes.client.JSONServiceBase;
import com.overto.gwt.hermes.client.RemoteJSONService;

/**
 * Generator of JSON RPC wrappers. 
 */
public class JSONProxyGenerator extends Generator {

	Set alreadyProcessed;

	SourceWriter wrapperSourceWriter;
	StringWriter debugWriter;
	int debugIndent;
	private final String tabString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";

	public JSONProxyGenerator() {
		alreadyProcessed = new HashSet(0);
	}

	public String generate(TreeLogger logger, GeneratorContext context,
			String typeName) throws UnableToCompleteException {
		if (!alreadyProcessed.contains(typeName)) {
			alreadyProcessed.add(typeName);
		}
		TypeOracle oracle = context.getTypeOracle();
		JClassType wrappedInterface = oracle.findType(typeName);
		if (!wrappedInterface.isAssignableTo(
				oracle.findType(RemoteJSONService.class.getCanonicalName()))) {
			logAndThrow(logger, "Type " + typeName + 
					" should inherit from RemoteJSONService.");
		}
		JClassType wrappedAsyncInterface = oracle.findType(typeName + "Async");
		checkInterfaces(logger, wrappedInterface, wrappedAsyncInterface);
		String wrapperClassName = simpleStubClassName(wrappedAsyncInterface
				.getName());
		wrapperSourceWriter = getSourceWriter(logger, context,
				wrappedAsyncInterface.getPackage().getName(), wrapperClassName,
				wrappedAsyncInterface.getName());
		if (logger.isLoggable(TreeLogger.ALL)) {
			debugWriter = new StringWriter();
		} else {
			debugWriter = null;
		}
		Set typesToDeserialize = new HashSet();
		Set typesToSerialize = new HashSet();
		if (wrapperSourceWriter != null) {
			indent();
				JMethod[] methods = wrappedAsyncInterface.getMethods();
				for (int i = 0; i < methods.length; i++) {
					try {
						generateMethodWrapper(context, typesToDeserialize,
								typesToSerialize, methods[i], 
								wrappedInterface.getMethods()[i].getReturnType());
					} catch (NotFoundException e) {
						throw new UnableToCompleteException();
					}
				}
	
				Object[] toDeserialize = typesToDeserialize.toArray();
				for (int i = 0; i < toDeserialize.length; i++) {
					generateTypeDeserializer((JType)toDeserialize[i]);
				}
				
				Object[] toSerialize = typesToSerialize.toArray();
				for (int i = 0; i < toSerialize.length; i++) {
					generateTypeSerializer((JType)toSerialize[i]);
				}
				outdent();
			wrapperSourceWriter.commit(logger);		
			if (logger.isLoggable(TreeLogger.ALL)) {
				logger.log(TreeLogger.ALL, debugWriter.toString(), null);
			}
		}
		return wrappedAsyncInterface.getPackage().getName() + "."
				+ wrapperClassName;
	}

	private void checkInterfaces(TreeLogger logger, JClassType wrappedInterface,
			JClassType wrappedAsyncInterface) throws UnableToCompleteException {
		if (wrappedAsyncInterface == null) {
			logAndThrow(logger, "Can't find async interface for " + 
					wrappedInterface.getQualifiedSourceName());
		}
		JMethod[] methods = wrappedInterface.getMethods();	
		JMethod[] asyncMethods = wrappedAsyncInterface.getMethods();
		if (methods.length != asyncMethods.length) {
			logAndThrow(logger, "Interfaces " + wrappedInterface.getQualifiedSourceName() +
					" and " + wrappedAsyncInterface.getQualifiedSourceName() +
					" should have the same number of methods.");
		}
		Map fullMethodsMap = createMethodsMap(logger, methods, false);
		Map fullAsyncMethodsMap = createMethodsMap(logger, asyncMethods, true);
		 
		Object[] fullMethodNames = fullMethodsMap.keySet().toArray();
		for (int i = 0; i < fullMethodNames.length; i++) {
			String methodName = (String) fullMethodNames[i];
			if (!fullAsyncMethodsMap.containsKey(methodName)) {
				logAndThrow(logger, "Couldn't find async method for " +
						getReadableMethodName(((JMethod)fullMethodsMap.get(methodName))));
			}			
		}
	}
	
	private Map createMethodsMap(TreeLogger logger, JMethod[] methods, 
			boolean fromAsyncInterface) throws UnableToCompleteException {
		Map methodsMap = new HashMap();
		for (int i = 0; i < methods.length; i++) {
			JMethod method = methods[i];
			if (fromAsyncInterface &&
				method.getReturnType() != JPrimitiveType.VOID &&
				!method.getReturnType().getQualifiedSourceName().equals(
						Cancellable.class.getCanonicalName())) {
				logAndThrow(logger, "Method: " + getReadableMethodName(method) + 
						" should return void or Cancellable.");
			}
			StringBuilder methodName = new StringBuilder(method.getName());
			JParameter[] params = method.getParameters();
			int paramsCount = params.length - (fromAsyncInterface ? 1 : 0);
			for (int j = 0; j < paramsCount; j++) {
				methodName.append(getSafeTypeName(params[j].getType()));
			}
			if (fromAsyncInterface &&
				(params.length == 0 || 
				 !params[params.length - 1].getType().getQualifiedSourceName().equals(
						 AsyncCallback.class.getCanonicalName()))) {
				logAndThrow(logger, "Method's " + getReadableMethodName(method) + 
						" last param should have be of type AsyncCallback");
			}
			methodsMap.put(methodName.toString(), method);
		}
		return methodsMap;
	}
	
	private static String getReadableMethodName(JMethod method) {
		return String.format("%1$s:%2$s", new Object[] {
				method.getEnclosingType().getQualifiedSourceName(),
				method.getReadableDeclaration(true, true, true, true, true)
			});
	}
	
	private static void logAndThrow(TreeLogger logger, String errorMessage) 
		throws UnableToCompleteException {
		logger.log(TreeLogger.ERROR, errorMessage, null);
		throw new UnableToCompleteException();
	}

	private void generateTypeSerializer(JType type) {
		String shortTypeName = type.getSimpleSourceName();
		println(String.format(
				"private JSONValue serialize_%1$s(%2$s __toserialize__) {",
				new Object[] { getSafeTypeName(type), shortTypeName }));
		indent();
			println("if (__toserialize__ == null) {");
			indent();
				println("return JSONNull.getInstance();");
				outdent();
			println("};");
			if (type.isArray() != null) {
				println("JSONArray __result__ = new JSONArray();");
				println("for (int __i__ = 0; __i__ < __toserialize__.length; __i__++) {");
				indent();
					println(String.format(
							"__result__.set(__i__, serialize_%1$s(__toserialize__[__i__]));",
							new Object[] {getSafeTypeName(((JArrayType)type).getComponentType())}));
					outdent();
				println("};");
			} else {
				println("JSONObject __result__ = new JSONObject();");
				JMethod[] methods = ((JClassType)type).getMethods();
				for (int i = 0; i < methods.length; i++) {
					JMethod method = methods[i];
					if (method.getParameters().length == 0) {
						String propertyName = null;
						String propertyAccessor = null;
						if (method.getName().startsWith("get")) {
							propertyName = method.getName().substring(3);
							propertyAccessor = "get";
						} else if ((method.getName().startsWith("is"))) {
							propertyName = method.getName().substring(2);
							propertyAccessor = "is";
						}
						if (propertyName != null)
							writeParam(null, "__result__", "__toserialize__." + propertyAccessor, 
									"()", new NameTypePair(propertyName, method.getReturnType()));
					}
				}
			}
			println("return __result__;");
			outdent();
		println("}");
	}

	private void generateTypeDeserializer(JType type) {
		String shortTypeName = type.getSimpleSourceName();
		println(String.format(
				"private %1$s deserialize_%2$s(JSONValue __deserialized__) {",
				new Object[] { shortTypeName, getSafeTypeName(type) }));
		indent();
			println("if (__deserialized__.isNull() != null) {");
			indent();
				println("return null;");
				outdent();
			println("};");
			if (type.isArray() != null) {
				JType componentType = ((JArrayType)type).getComponentType();
				println("JSONArray __array__ = (JSONArray)__deserialized__;");
				println(String.format("%1$s[] __result__ = new %1$s[__array__.size()];",
					new Object[] { componentType.getSimpleSourceName() }));
				println("for (int __i__ = 0; __i__ < __array__.size(); __i__++) {");
				indent();
					println(String.format("__result__[__i__] = deserialize_%1$s(__array__.get(__i__));",
							new Object[] {getSafeTypeName(componentType)}));
					outdent();
				println("};");
			} else {
				println("JSONObject __object__ = (JSONObject)__deserialized__;");
				println(String.format("%1$s __result__ = new %1$s();",
					new Object[] { shortTypeName }));
				println("JSONValue __value__ = null;");
				JMethod[] methods = ((JClassType)type).getMethods();
				for (int i = 0; i < methods.length; i++) {
					JMethod method = methods[i];
					if (method.getParameters().length == 1) {
						if (method.getName().startsWith("set")) {
							String propertyName = method.getName().substring(3);
							println("__value__ = __object__.get(\"" + propertyName + "\");");
							println("if (__value__ != null) {");
							indent();
								println(String.format("__result__.%1$s(deserialize_%2$s(__value__));",
										new Object[] {method.getName(), 
										getSafeTypeName(method.getParameters()[0].getType()),
										method.getParameters()[0].getType().getSimpleSourceName()}));
								outdent();
							println("};");
						}
					}
				}
			}
			println("return __result__;");
			outdent();
		println("}");
	}

	private void generateMethodWrapper(GeneratorContext context, Set typesToDeserialize,
			Set typesToSerialize, JMethod asyncMethod, JType returnType) throws NotFoundException {
		boolean returnsCancellable =
			asyncMethod.getReturnType() != JPrimitiveType.VOID;  
		print("public " + (returnsCancellable ? "Cancellable " : "void "));
		print(asyncMethod.getName() + "(");
		JParameter[] originalParams = asyncMethod.getParameters();
		NameTypePair[] params = new NameTypePair[originalParams.length - 1];
		String asyncCallbackName = originalParams[originalParams.length - 1].getName();
		for (int i = 0; i < originalParams.length; i++) {
			if (i > 0) {
				print(", ");
			}
			print(originalParams[i].getType().getSimpleSourceName());
			print(" ");
			print(originalParams[i].getName());
			if (i < originalParams.length - 1) {
				params[i] = new NameTypePair(originalParams[i].getName(),
						originalParams[i].getType());
			}
		}
		println(") {");
		indent();
			println("JSONObject __params__ = new JSONObject();");
			for (int i = 0; i < params.length; i++) {
				writeParam(typesToSerialize, "__params__", "", "", params[i]);
			}
			println(String.format("if (%1$s instanceof DebugAsyncCallback) {",
					new Object[]{asyncCallbackName}));
			indent();
				println(String.format("((DebugAsyncCallback)%1$s).afterSerialize(__params__.toString());", 
						new Object[]{asyncCallbackName}));
				outdent();
			println("}");
			println("RequestBuilder __builder__ = createBuilder(\"" + asyncMethod.getName() + "\");");
			createDeserializingCallback(context, typesToDeserialize, returnType, asyncCallbackName);
	
			println("try {");
			indent();
			    if (returnsCancellable) {
			    	print("return new CancellableRequest(");
			    }
				print("__builder__.sendRequest(__params__.toString(), __callback__)");
				if (returnsCancellable) {
					println(");");
				} else {
					println(";");
				}
				outdent();
				println("} catch(RequestException __exception__) {");
				indent();
				println("callback.onFailure(__exception__);");				
				outdent();
				println("};");
				outdent();
			if (returnsCancellable) {
		    	println("return null;");
		    }
			println("}");

	}

	private void createDeserializingCallback(GeneratorContext context, Set typesToDeserialize,
			JType returnType, String asyncCallbackName) throws NotFoundException {
		String returnTypeName = returnType.getSimpleSourceName();
		
		
		println("JSONRequestCallback __callback__ = new JSONRequestCallback(" +
				asyncCallbackName + ") {");
		indent();
		println("public Object deserialize(String __response__) {");
		indent();
		if (returnTypeName == "void") {
			println("return null;");
		} else {
			println("JSONValue __deserialized__ = JSONParser.parse(__response__);");
			String safeTypeName;
			if (returnType.isPrimitive() != null) {
				safeTypeName = getSafeTypeName(mapPrimitiveToObject(context, returnType));
			} else {
				safeTypeName = getSafeTypeName(returnType);
			}
			println("return deserialize_" + safeTypeName
					+ "(__deserialized__);");
			
			if (typesToDeserialize != null) {
				traverseAllSubTypes(typesToDeserialize, returnType);			
			}
		}
		outdent();
		println("}");
		outdent();
		println("};");
	}

	private JType mapPrimitiveToObject(GeneratorContext context, JType type) throws NotFoundException {
		if (type == JPrimitiveType.BOOLEAN) {
			return context.getTypeOracle().getType("java.lang.Boolean");
		}
		if (type == JPrimitiveType.BYTE) {
			return context.getTypeOracle().getType("java.lang.Byte");
		}
		if (type == JPrimitiveType.SHORT) {
			return context.getTypeOracle().getType("java.lang.Short");
		}
		if (type == JPrimitiveType.INT) {
			return context.getTypeOracle().getType("java.lang.Integer");
		}
		if (type == JPrimitiveType.LONG) {
			return context.getTypeOracle().getType("java.lang.Long");
		}
		if (type == JPrimitiveType.FLOAT) {
			return context.getTypeOracle().getType("java.lang.Float");
		}
		if (type == JPrimitiveType.DOUBLE) {
			return context.getTypeOracle().getType("java.lang.Double");
		}
		if (type == JPrimitiveType.CHAR) {
			return context.getTypeOracle().getType("java.lang.Character");
		}
		return (JType)null;
	}

	private boolean isPrimitiveOrStringType(JType type) {
		return type instanceof JPrimitiveType || 
		type.getQualifiedSourceName().equals("java.lang.String"); 
	}

	private String getSafeTypeName(JType type) {
		if (type.isArray() != null) {
			return "array_" + getSafeTypeName(((JArrayType)type).getComponentType());
		}
		return type.getQualifiedSourceName().replace('.',	'_');
	}

	private void writeParam(Set typesToSerialize, String JSONObjectName, 
			String paramPrefix, String paramSufix,	NameTypePair param) {
		println(String.format("%1$s.put(\"%2$s\", serialize_%3$s(%4$s%2$s%5$s));",
				new Object[] {JSONObjectName, param.Name, getSafeTypeName(param.Type),
							paramPrefix, paramSufix}));
		
		if (typesToSerialize != null) {
			traverseAllSubTypes(typesToSerialize, param.Type);			
		}
	}
	
	private boolean traverseAllSubTypes(Set types, JType type) {
		if (isPrimitiveOrStringType(type)) {
			return false;
		} else {
			if (types.contains(type))
				return false;
			types.add(type);
			if (type.isArray() != null) {
				traverseAllSubTypes(types, ((JArrayType)type).getComponentType());
			}
			
			if (type.isClass() != null) {
				JMethod[] methods = ((JClassType)type).getMethods();
				for (int i = 0; i < methods.length; i++) {
					JMethod method = methods[i];
					traverseAllSubTypes(types, method.getReturnType());
					JParameter[] params = method.getParameters();
					for (int j = 0; j < params.length; j++) {
						JParameter param = params[j];
						traverseAllSubTypes(types, param.getType());
					}
				}
			}
			return true;
		}
	}

	private SourceWriter getSourceWriter(TreeLogger logger,
			GeneratorContext context, String packageName, String className,
			String interfaceName) {
		PrintWriter printWriter = context.tryCreate(logger, packageName,
				className);
		if (printWriter == null) {
			return null;
		}
		ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
				packageName, className);

		composerFactory.addImport("com.overto.gwt.hermes.client.Cancellable");
		composerFactory.addImport("com.overto.gwt.hermes.client.CancellableRequest");
		composerFactory.addImport("com.overto.gwt.hermes.client.JSONServiceBase");
		composerFactory.addImport("com.overto.gwt.hermes.client.JSONRequestCallback");
		composerFactory.addImport("com.overto.gwt.hermes.client.DebugAsyncCallback");
		composerFactory.addImport("com.google.gwt.user.client.rpc.AsyncCallback");
		composerFactory.addImport("com.google.gwt.user.client.rpc.ServiceDefTarget");
		composerFactory.addImport("com.google.gwt.http.client.RequestBuilder");		
		composerFactory.addImport("com.google.gwt.http.client.RequestException");
		composerFactory.addImport("com.google.gwt.json.client.JSONParser");
		composerFactory.addImport("com.google.gwt.json.client.JSONValue");
		composerFactory.addImport("com.google.gwt.json.client.JSONArray");
		composerFactory.addImport("com.google.gwt.json.client.JSONBoolean");
		composerFactory.addImport("com.google.gwt.json.client.JSONNull");
		composerFactory.addImport("com.google.gwt.json.client.JSONNumber");
		composerFactory.addImport("com.google.gwt.json.client.JSONObject");
		composerFactory.addImport("com.google.gwt.json.client.JSONString");
		composerFactory.addImport("com.google.gwt.user.client.Window");
		composerFactory.addImport("com.overto.gwt.hermes.client.JSONInteger");

		composerFactory.setSuperclass(JSONServiceBase.class.getName());
		composerFactory.addImplementedInterface(interfaceName);
		composerFactory.addImplementedInterface(ServiceDefTarget.class
				.getSimpleName());

		return composerFactory.createSourceWriter(context, printWriter);
	}

	private void print(String s) {
		wrapperSourceWriter.print(s);
		if (debugWriter != null) {
			debugWriter.append(s);
		}
	}

	private void println(String s) {
		wrapperSourceWriter.println(s);
		if (debugWriter != null) {
			debugWriter.append(s);
			debugWriter.append('\n');
			if (debugIndent > 0)
				debugWriter.append(tabString.substring(0, debugIndent));
		}
	}

	private void indent() {
		wrapperSourceWriter.indent();
		debugIndent++;
	}

	private void outdent() {
		wrapperSourceWriter.outdent();
		debugIndent--;
	}

	private static String simpleStubClassName(String className) {
		return "__" + className + "_impl__";
	}

	private class NameTypePair {
		public final String Name;
		public final JType Type;

		public NameTypePair(String name, JType type) {
			Name = name;
			Type = type;
		}
	}
}
